﻿using MyHome.Server.PluginBase;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace MyHome.Server.PluginManagement
{
    /// <summary>
    /// A host for all plugins.
    /// </summary>
    public class PluginHost
    {
        /// <summary>
        /// Gets the plugin connections.
        /// </summary>
        /// <value>
        /// The plugin connections.
        /// </value>
        public virtual ObservableCollection<IPluginConnection<object>> PluginConnections
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the plugins.
        /// </summary>
        /// <value>
        /// The plugins.
        /// </value>
        public virtual ObservableCollection<IPlugin> Plugins
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the plugin types.
        /// </summary>
        /// <value>
        /// The plugin types.
        /// </value>
        public virtual IDictionary<string, Type> PluginTypes
        {
            get;
            private set;
        }

        public PluginHost()
        {
            var repository = new DataAccess.Repository();
            bool autoLoadPlugins = bool.Parse(repository.Configuration["AutoLoadPlugins"]);

            if (autoLoadPlugins)
            {
                Load();
                bool autoStartPlugins = bool.Parse(repository.Configuration["AutoStartPlugins"]);
                if (autoStartPlugins)
                    Start();
            }
        }

        private async void Load()
        {
            await LoadAsync();
        }

        /// <summary>
        /// Loads the all types, plugins and plugin connections asynchronous.
        /// </summary>
        public async virtual Task LoadAsync()
        {
            PluginTypes = new ReadOnlyDictionary<string, Type>(await PluginAssemblyHelper.LoadFromDiskAsync());
            Plugins = new ObservableCollection<IPlugin>(await PluginDatabaseHelper.LoadPluginsFromDatabaseAsync(PluginTypes));
            PluginConnections = new ObservableCollection<IPluginConnection<object>>(await PluginDatabaseHelper.LoadPluginConnectionsFromDatabaseAsync(Plugins));
        }

        /// <summary>
        /// Starts all plugins asynchronous.
        /// </summary>
        public virtual Task StartAsync()
        {
            return Task.Run(() => Start());
        }

        private void Start()
        {
            // we start plugins from output to input so we do not run into problems by input plugins
            // generating values before the output plugins are ready.

            var outputPlugins = Plugins.Where(p => p.PluginType == PluginType.Output);
            var processingPlugins = Plugins.Where(p => p.PluginType == PluginType.Processing);
            var inputOutputPlugins = Plugins.Where(p => p.PluginType == PluginType.InputOutput);
            var inputPlugins = Plugins.Where(p => p.PluginType == PluginType.Input);

            Parallel.ForEach(outputPlugins, async p => await p.StartAsync());
            Parallel.ForEach(processingPlugins, async p => await p.StartAsync());
            Parallel.ForEach(inputOutputPlugins, async p => await p.StartAsync());
            Parallel.ForEach(inputPlugins, async p => await p.StartAsync());
        }

        /// <summary>
        /// Stops all plugins asynchronous.
        /// </summary>
        public virtual Task StopAsync()
        {
            return Task.Run(() => Stop());
        }

        private void Stop()
        {
            // we Stop plugins from input to output so we do not run into problems by input plugins
            // generating values after the output plugins have been stopped.

            var outputPlugins = Plugins.Where(p => p.PluginType == PluginType.Output);
            var processingPlugins = Plugins.Where(p => p.PluginType == PluginType.Processing);
            var inputOutputPlugins = Plugins.Where(p => p.PluginType == PluginType.InputOutput);
            var inputPlugins = Plugins.Where(p => p.PluginType == PluginType.Input);

            Parallel.ForEach(inputPlugins, async p => await p.StopAsync());
            Parallel.ForEach(inputOutputPlugins, async p => await p.StopAsync());
            Parallel.ForEach(processingPlugins, async p => await p.StopAsync());
            Parallel.ForEach(outputPlugins, async p => await p.StopAsync());
        }

        /// <summary>
        /// Starts the plugin asynchronous.
        /// </summary>
        /// <param name="plugin">The plugin.</param>
        /// <returns></returns>
        public virtual Task StartPluginAsync(IPlugin plugin)
        {
            return StartPluginAsync(plugin.Id);
        }

        /// <summary>
        /// Starts the plugin asynchronous.
        /// </summary>
        /// <param name="pluginId">The plugin unique identifier.</param>
        /// <returns></returns>
        public virtual Task StartPluginAsync(int pluginId)
        {
            var possible = Plugins.Where(p => p.Id == pluginId);
            if (possible.Count() == 1)
            {
                var plugin = possible.First();
                return plugin.StartAsync();
            }
            else
            {
                Trace.TraceError("Unable to start plugin with Id {0} as it was not found.", pluginId);
                return Task.Run(() => { });
            }
        }

        /// <summary>
        /// Stops the plugin asynchronous.
        /// </summary>
        /// <param name="plugin">The plugin.</param>
        /// <returns></returns>
        public virtual Task StopPluginAsync(IPlugin plugin)
        {
            return StopPluginAsync(plugin.Id);
        }

        /// <summary>
        /// Stops the plugin asynchronous.
        /// </summary>
        /// <param name="pluginId">The plugin unique identifier.</param>
        /// <returns></returns>
        public virtual Task StopPluginAsync(int pluginId)
        {
            var possible = Plugins.Where(p => p.Id == pluginId);
            if (possible.Count() == 1)
            {
                var plugin = possible.First();
                return plugin.StopAsync();
            }
            else
            {
                Trace.TraceError("Unable to stop plugin with Id {0} as it was not found.", pluginId);
                return Task.Run(() => { });
            }
        }

        public IPlugin AddPlugin(string type, IDictionary<string, string> configuration, int x, int y)
        {
            int id = Plugins.Max(p => p.Id) + 1;
            Type pluginType;
            if (!PluginTypes.TryGetValue(type, out pluginType))
            {
                Trace.TraceError("Cannot add plugin of type {0} because the type is unknown.", type);
                return null;
            }

            var plugin = PluginDatabaseHelper.AddPlugin(pluginType, id, configuration, x, y);
            Plugins.Add(plugin);

            return plugin;
        }

        public async void DeletePlugin(int pluginId)
        {
            var plugin = Plugins.SingleOrDefault(p => p.Id == pluginId);
            if (plugin == null)
            {
                Trace.TraceError("Unable to delete plugin with Id {0} as it was not found.", pluginId);
                return;
            }

            Plugins.Remove(plugin);
            PluginDatabaseHelper.DeletePlugin(pluginId);
            await plugin.StopAsync();

            var assocciatedPluginConnections = PluginConnections.Where(p => p.Input == plugin || p.Output == plugin).ToList();
            foreach (var pluginConnection in assocciatedPluginConnections)
            {
                PluginConnections.Remove(pluginConnection);
                PluginDatabaseHelper.DeletePluginConnection(pluginConnection.Id);
                pluginConnection.Input = null;
                pluginConnection.Output = null;
            }
        }

        public void UpdatePlugin(int pluginId, IDictionary<string, string> configuration, int x, int y)
        {
            var plugin = Plugins.SingleOrDefault(p => p.Id == pluginId);
            if (plugin == null)
            {
                Trace.TraceError("Unable to update plugin with Id {0} as it was not found.", pluginId);
                return;
            }

            plugin.Configuration = configuration;
            plugin.X = x;
            plugin.Y = y;
            PluginDatabaseHelper.UpdatePlugin(plugin);
        }

        public IPluginConnection<object> AddPluginConnection(int inputPluginId, int outputPluginId)
        {
            int id = PluginConnections.Max(p => p.Id) + 1;
            var input = Plugins.SingleOrDefault(p => p.Id == inputPluginId);
            var output = Plugins.SingleOrDefault(p => p.Id == outputPluginId);
            if (input == null || output == null)
            {
                if (input == null)
                    Trace.TraceError("Cannot create plugin connection with Id {0} as input, as it was not found.", inputPluginId);

                if (output == null)
                    Trace.TraceError("Cannot create plugin connection with Id {0} as output, as it was not found.", outputPluginId);

                return null;
            }

            var pluginConnection = PluginDatabaseHelper.AddPluginConnection(id, input, output);
            PluginConnections.Add(pluginConnection);

            return pluginConnection;
        }

        public void DeletePluginConnection(int pluginConnectionId)
        {
            var pluginConnection = PluginConnections.SingleOrDefault(p => p.Id == pluginConnectionId);
            if (pluginConnection == null)
            {
                Trace.TraceError("Unable to delete plugin connection with Id {0} as it was not found.", pluginConnectionId);
                return;
            }

            PluginConnections.Remove(pluginConnection);
            PluginDatabaseHelper.DeletePluginConnection(pluginConnectionId);
            pluginConnection.Input = null;
            pluginConnection.Output = null;
        }

        public void UpdatePluginConnection(int pluginConnectionId, int inputPluginId, int outputPluginId)
        {
            var pluginConnection = PluginConnections.SingleOrDefault(p => p.Id == pluginConnectionId);
            if (pluginConnection == null)
            {
                Trace.TraceError("Unable to update plugin connection with Id {0} as it was not found.", pluginConnectionId);
                return;
            }

            var input = Plugins.SingleOrDefault(p => p.Id == inputPluginId);
            var output = Plugins.SingleOrDefault(p => p.Id == outputPluginId);
            if (input == null || output == null)
            {
                if (input == null)
                    Trace.TraceError("Cannot update plugin connection with Id {0} using Id {1} as input, as it was not found.", pluginConnectionId, inputPluginId);

                if (output == null)
                    Trace.TraceError("Cannot update plugin connection with Id {0} using Id {1} as output, as it was not found.", pluginConnectionId, outputPluginId);

                return;
            }

            var inputPlugin = input as IInputPlugin<object>;
            var outputPlugin = output as IOutputPlugin<object>;
            if (inputPlugin == null || outputPlugin == null)
            {
                if (inputPlugin == null)
                    Trace.TraceError("Cannot update plugin connection with Id {0} using {1} as input, as it is no input plugin.", pluginConnectionId, inputPlugin);

                if (outputPlugin == null)
                    Trace.TraceError("Cannot update plugin connection with Id {0} using {1} as output, as it is no output plugin.", pluginConnectionId, outputPlugin);

                return;
            }

            if (inputPlugin != pluginConnection.Input)
                pluginConnection.Input = inputPlugin;

            if (outputPlugin != pluginConnection.Output)
                pluginConnection.Output = outputPlugin;

            PluginDatabaseHelper.UpdatePluginConnection(pluginConnection);
        }
    }
}